import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
import { FlatTreeControl } from '@angular/cdk/tree';
import { SelectionModel } from '@angular/cdk/collections';
import { NzContextMenuService, NzDropdownMenuComponent } from 'ng-zorro-antd/dropdown';
import { Observable } from 'rxjs';

export type DefaultTreeNode = {
  key: string;
  name: string;
  disabled?: boolean;
};

export type DefaultFlatNode = {
  key: string;
  name: string;
  level: number;
  expandable: boolean;
  disabled: boolean;
};

export type EntityTreeNode<T> = T &
  DefaultTreeNode & {
    children?: EntityTreeNode<T>[];
  };

export type EntityFlatNode<T> = T & DefaultFlatNode;

export abstract class TreeComponent<T> {
  flatNodeMap: Map<EntityFlatNode<T>, EntityTreeNode<T>>;
  nestedNodeMap: Map<EntityTreeNode<T>, EntityFlatNode<T>>;
  selectListSelection: SelectionModel<EntityFlatNode<T>>;
  treeControl: FlatTreeControl<EntityFlatNode<T>>;
  treeFlattener: NzTreeFlattener<EntityTreeNode<T>, EntityFlatNode<T>, EntityFlatNode<T>>;
  dataSource: NzTreeFlatDataSource<EntityTreeNode<T>, EntityFlatNode<T>, EntityFlatNode<T>>;
  treeData: EntityTreeNode<T>[] = [];
  currentNode: EntityFlatNode<T>;

  constructor(protected multiple: boolean, protected nzContextMenuService: NzContextMenuService) {
    this.flatNodeMap = new Map<EntityFlatNode<T>, EntityTreeNode<T>>();
    this.nestedNodeMap = new Map<EntityTreeNode<T>, EntityFlatNode<T>>();
    this.selectListSelection = new SelectionModel<EntityFlatNode<T>>(multiple);
    this.treeControl = new FlatTreeControl<EntityFlatNode<T>>(
      node => node.level,
      node => node.expandable
    );

    this.treeFlattener = new NzTreeFlattener(
      this.transformer,
      node => node.level,
      node => node.expandable,
      node => node.children
    );
    this.dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
  }

  transformer = (node: EntityTreeNode<T>, level: number): EntityFlatNode<T> => {
    const existingNode = this.nestedNodeMap.get(node);
    const flatNode =
      existingNode && existingNode.key === node.key
        ? existingNode
        : {
            ...node,
            name: node.name,
            key: node.key,
            level,
            expandable: !!node.children?.length,
            disabled: !!node.disabled,
          };
    flatNode.name = node.name;
    this.flatNodeMap.set(flatNode, node);
    this.nestedNodeMap.set(node, flatNode);
    return flatNode;
  };
  hasChild = (_: number, node: EntityFlatNode<T>): boolean => node.expandable;
  trackBy = (_: number, node: EntityFlatNode<T>): string =>
    `${node.key}-${node.name}-${node.expandable}`;

  createEntityTreeNodes(
    list: T[],
    parent: T,
    parentIdPropretyName?: string,
    idPropertyName?: string
  ): EntityTreeNode<T>[] {
    let treeNodes: EntityTreeNode<T>[] = [];
    let parentIdKey = !parentIdPropretyName ? 'parentId' : parentIdPropretyName;
    let idKey = !idPropertyName ? 'id' : idPropertyName;
    let parentId = !parent ? null : parent[idKey];
    for (let node of list.filter(x => x[parentIdKey] == parentId)) {
      const children = this.createEntityTreeNodes(list, node, parentIdKey, idKey);

      treeNodes = [
        ...treeNodes,
        {
          ...node,
          children,
          ...this.createEntityTreeNode(node),
        },
      ];
    }
    return treeNodes;
  }

  toggleExpand(node: EntityFlatNode<T>) {
    if (this.treeControl.isExpanded(node)) {
      this.treeControl.collapse(node);
    } else {
      this.treeControl.expand(node);
    }
  }

  contextMenu($event: MouseEvent, menu: NzDropdownMenuComponent, node: EntityFlatNode<T>) {
    this.selectListSelection.select(node);
    this.nzContextMenuService.create($event, menu);
  }

  selectNode(node: EntityFlatNode<T>) {
    this.selectListSelection.select(node);
  }

  get selectedNode(): EntityFlatNode<T> {
    return this.selectListSelection.selected[0];
  }

  abstract createEntityTreeNode(model: T): EntityTreeNode<T>;
}

export abstract class EditableTreeComponent<T extends { id: string }> extends TreeComponent<T> {
  add(parentId: string) {
    this.refEntityDetail().subscribe(model => {
      if (!!model) {
        return this.createEntity(model, parentId).subscribe(result => {
          let node = this.treeControl.dataNodes.find(x => x.id == parentId);
          const parentNode = this.flatNodeMap.get(node);
          if (parentNode) {
            parentNode.children = parentNode.children || [];
            parentNode.children.push({
              ...this.createEntityTreeNode(result),
            });
            node.expandable = true;
          } else {
            this.treeData.push({
              ...this.createEntityTreeNode(result),
            });
          }
          this.dataSource.setData(this.treeData);
          this.treeControl.expand(node);
        });
      }
    });
  }

  edit(node: EntityFlatNode<T>) {
    this.refEntityDetail(node).subscribe(model => {
      if (!!model) {
        this.updateEntity(node.id, model).subscribe(result => {
          let treeNode = this.flatNodeMap.get(node);
          let resultTreeNode = this.createEntityTreeNode(result);
          for (let key of Object.keys(resultTreeNode)) {
            if (key == 'level') continue;
            if (key == 'expandable') continue;

            treeNode[key] = resultTreeNode[key];
            node[key] = resultTreeNode[key];
          }

          this.dataSource.setData(this.treeData);
        });
      }
    });
  }

  delete(node: EntityFlatNode<T>) {
    this.deleteEntity(node.id).subscribe(_ => {
      if (!!node['parentId']) {
        let parent = this.treeControl.dataNodes.find(x => x.id == node['parentId']);
        const parentNode = this.flatNodeMap.get(parent);
        parentNode.children = parentNode.children.filter(x => x.id != node.id);
        parent.expandable = !!parentNode.children.length;
      } else {
        this.treeData = this.treeData.filter(x => x.id != node.id);
      }

      this.dataSource.setData(this.treeData);
    });
  }

  abstract refEntityDetail(model?: T): Observable<any>;
  abstract createEntity(model: any, parentId: string): Observable<T>;
  abstract updateEntity(id: string, model: any): Observable<T>;
  abstract deleteEntity(id: string): Observable<any>;
}
